三种线程安全的List

您所在的位置:网站首页 shopping list怎么读的 三种线程安全的List

三种线程安全的List

2024-07-15 20:58:29| 来源: 网络整理| 查看: 265

在单线程开发环境中,我们经常使用ArrayList作容器来存储我们的数据,但它不是线程安全的,在多线程环境中使用它可能会出现意想不到的结果。

多线程中的ArrayList:

我们可以从一段代码了解并发环境下使用ArrayList的情况:

public class ConcurrentArrayList { public static void main(String[] args) throws InterruptedException { List list = new ArrayList(); Runnable runnable = () -> { for (int i = 0; i new Thread(runnable).start(); } Thread.sleep(500); System.out.println(list.size()); } }

代码中循环创建了两个线程,这两个线程都执行10000次数组的添加操作,理论上最后输出的结果应该为20000,但经过多次尝试,最后只出现了两种结果:

数组索引越界异常 Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 10 at java.util.ArrayList.add(ArrayList.java:463) at ConcurrentArrayList.lambda$main$0(ConcurrentArrayList.java:14) at java.lang.Thread.run(Thread.java:748) 10007 输出结果小于20000 16093

虽然仍有可能得到20000的结果,但概率非常低。我们要从ArrayList的源码中去分析为什么会出现这种结果。 ArrayList数组默认初始化大小:

// 默认初始大小 private static final int DEFAULT_CAPACITY = 10; ... // 数组size private int size;

ArrayList的add方法:

public boolean add(E e) { //确定集合的大小是否足够,如果不够则会进行扩容 ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }

以上面错误1:ArrayIndexOutOfBoundsException: 10为例,出现错误的步骤如下:

假设某时刻Thread-0和Thread-1都执行到了elementData[size++] = e; 这步,获取的size大小都为9,此时轮到Thread-1执行Thread-1执行elementData[9] = e,空间刚刚好够用,赋值完后size变为10。接着轮到Thread-0执行因为Thread-0已经跳过了ensureCapacityInternal(size + 1); 这步判断容量的检查步骤,因此它执行elementData[10] = e,而数组容量刚好为10!此时就出现了数组越界的错误。

另外,size++本身就是非原子性的,多个线程之间访问冲突,这时两个线程可能对同一个位置赋值,这就出现了出现size小于期望值的错误2结果。

线程安全的List

目前比较常用的构建线程安全的List有三种方法:

使用Vector容器使用Collections的静态方法synchronizedList(List< T> list)采用CopyOnWriteArrayList容器 1.使用Vector容器

Vector类实现了可扩展的对象数组,并且它是线程安全的。它和ArrayList在常用方法的实现上很相似,不同的只是它采用了同步关键词synchronized修饰方法。 ArrayList中的add方法:

public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }

Vector中的add方法:

public void add(int index, E element) { insertElementAt(element, index); } ... // 使用了synchronized关键词修饰 public synchronized void insertElementAt(E obj, int index) { modCount++; if (index > elementCount) { throw new ArrayIndexOutOfBoundsException(index + " > " + elementCount); } ensureCapacityHelper(elementCount + 1); System.arraycopy(elementData, index, elementData, index + 1, elementCount - index); elementData[index] = obj; elementCount++; }

可以看出,Vector在通用方法的实现上ArrayList并没有什么区别(这里不比较扩容方式等细节)

2. Collections.synchronizedList(List< T> list)

使用这种方法我们可以获得线程安全的List容器,它和Vector的区别在于它采用了同步代码块实现线程间的同步。通过分析源码,它的底层使用了新的容器包装原始的List。 下图是新容器的继承关系图: 在这里插入图片描述 synchronizedList方法:

public static List synchronizedList(List list) { return (list instanceof RandomAccess ? new SynchronizedRandomAccessList(list) : new SynchronizedList(list)); }

因为ArrayList实现了RandomAccess接口,因此该方法返回一个SynchronizedRandomAccessList实例。 该类的add实现:

public void add(int index, E element) { synchronized (mutex) {list.add(index, element);} }

其中,mutex是final修饰的一个对象:

final Object mutex;

我们可以看到,这种线程安全容器是通过同步代码块来实现的,基础的add方法任然是由ArrayList实现。

我们再来看看它的读方法:

public E get(int index) { synchronized (mutex) {return list.get(index);} }

和写方法没什么区别,同样是使用了同步代码块。线程同步的实现原理非常简单!

通过上面的分析可以看出,无论是读操作还是写操作,它都会进行加锁,当线程的并发级别非常高时就会浪费掉大量的资源,因此某些情况下它并不是一个好的选择。针对这个问题,我们引出第三种线程安全容器的实现。

3. CopyOnWriteArrayList

顾名思义,它的意思就是在写操作的时候复制数组。为了将读取的性能发挥到极致,在该类的使用过程中,读读操作和读写操作都不互斥,这是一个很神奇的操作,接下来我们看看它如何实现。

public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; // 复制数组 Object[] newElements = Arrays.copyOf(elements, len + 1); // 赋值 newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }

从CopyOnWriteArrayList的add实现方式可以看出它是通过lock来实现线程间的同步的,这是一个标准的lock写法。那么它是怎么做到读写互斥的呢?

// 复制数组 Object[] newElements = Arrays.copyOf(elements, len + 1); // 赋值 newElements[len] = e;

真实实现读写互斥的细节就在这两行代码上。在面临写操作的时候,CopyOnWriteArrayList会先复制原来的数组并且在新数组上进行修改,最后再将原数组覆盖。如果写操作的过程中发生了线程切换,并且切换到读线程,因为此时数组并未发生覆盖,读操作读取的还是原数组。

换句话说,就是读操作和写操作位于不同的数组上,因此它们不会发生安全问题。

另外,数组定义private transient volatile Object[] array,其中采用volatile修饰,保证内存可见性,读取线程可以马上知道这个修改。

private transient volatile Object[] array; 三种方式的性能比较 1. 首先我们来看看三种方式在写操作的情况: public class ConcurrentList { public static void main(String[] args) { testVector(); testSynchronizedList(); testCopyOnWriteArrayList(); } public static void testVector(){ Vector vector = new Vector(); long time1 = System.currentTimeMillis(); for (int i = 0; i List list = Collections.synchronizedList(new ArrayList()); long time1 = System.currentTimeMillis(); for (int i = 0; i CopyOnWriteArrayList list = new CopyOnWriteArrayList(); long time1 = System.currentTimeMillis(); for (int i = 0; i public static void main(String[] args) { testVector(); testSynchronizedList(); testCopyOnWriteArrayList(); } public static void testVector(){ Vector vector = new Vector(); vector.add(0); long time1 = System.currentTimeMillis(); for (int i = 0; i List list = Collections.synchronizedList(new ArrayList()); list.add(0); long time1 = System.currentTimeMillis(); for (int i = 0; i CopyOnWriteArrayList list = new CopyOnWriteArrayList(); list.add(0); long time1 = System.currentTimeMillis(); for (int i = 0; i


【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭